package orbito.simulation;

import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.geom.AffineTransform;

import orbito.GameController;
import orbito.graphics.Viewport;
import orbito.util.Stopwatch;

public class UnthreadedSimulator implements Simulator {
	
	// The game controller.
	GameController game;
	
	// If the sim is paused.
	boolean isPaused;
	
	// If the simulation in running in-real time, this is set to true, false otherwise.
	boolean isRunningRealTime;
	
	// The initially set dt, the current dt.
	final double initDt;
	double dt;
	
	// The average dt, for display.
	double displayDt;
	
	// The time the simulation has run, in seconds.
	double simTime;

	// The time accumulator for accumulating enough sim-time to perform a step.
	double simTimeAccum;
	
	// The time compression.
	double timeCompression;
	
	// The overall number of simulation steps performed.
	long stepCount;
	
	// Step size lock in down and up directions.
	boolean dtDownLock = false, dtUpLock = false;
	
	// Stopwatches for timing and benchmarking the simulation.
	Stopwatch sw, benchSw;
	
	public UnthreadedSimulator(double initDt) {
		// Set initial dt.
		this.initDt = initDt;
		
		// Init simulator.
		init();
	}
	
	public void init() {
		// Init time variables.
		dt = initDt;
		simTime = 0;
		simTimeAccum = 0;
		timeCompression = 1;
		stepCount = 0;
		
		// Init sim states.
		isPaused = true;
		isRunningRealTime = true;
		
		// Init stopwatches.
		sw = new Stopwatch();
		benchSw = new Stopwatch();
	}
	
	public void step() {
		// Only step if unpaused.
		if (!isPaused) {
			// Determine the time passed since the last frame.
			double realElapsedTime = sw.getElapsedTime();
			
			// Reset the benchmark stopwatch.
			benchSw.reset();
			
			// Determine the sim time passed and schedule it for simulation.
			double simElapsedTime = timeCompression * realElapsedTime;
			simTimeAccum += simElapsedTime;
			
			double simTimeToProcess = simTimeAccum;
			
			while (simTimeAccum > dt) {
				game.step(simTime, dt);

				simTimeAccum -= dt;
				simTime += dt;
				stepCount++;

				if (benchSw.peekElapsedTime() * timeCompression > simTimeToProcess * 0.75) {
					dt *= 1.1;
					if (dtUpLock) {
						simTimeAccum = 0;
					}
				}
				
				if (dtUpLock && dt > initDt) {
					dt = initDt;
				}
			}
			
			if (benchSw.peekElapsedTime() * timeCompression < simTimeToProcess * 0.25) {
				dt *= 0.9;
			}
			
			if (dtDownLock && dt < initDt) {
				dt = initDt;
			}
			
			// Smooth the display dt.
			displayDt = displayDt * 0.95 + dt * 0.05;
		}
	}
	
	public void renderInfo(Graphics2D g, Viewport v, AffineTransform tx, double t, double dt) {
		AffineTransform tr = g.getTransform();
		int labelX = 0, valueX = 100, labelY = 0, h = g.getFontMetrics().getHeight();

		// Translate to upper right side of viewport.
		g.translate(v.getWidth() - 250, 30);
		
		String elapsedLabel = "Elapsed Time:";
		String elapsedValue = String.format("%.2f s", simTime);
		
		String tcmpLabel = "Time Compression:";
		String tcmpValue = timeCompression < 1 ? 
				String.format("1/%.0fx", 1 / timeCompression) :
				String.format("%.0fx", timeCompression);
		
		String stepLabel = "Time Step:";
		String stepValue = String.format("%.6f s", displayDt);
		
		String accumLabel = "Accumulator:";
		String accumValue = String.format("%.6f s", simTimeAccum);
		
		String stepCountLabel = "Step Count:";
		String stepCountValue = String.format("%d", stepCount);
		
		g.setColor(Color.ORANGE);
		g.drawString(elapsedLabel, labelX, labelY += h);
		g.drawString(tcmpLabel, labelX, labelY += h);
		g.drawString(stepLabel, labelX, labelY += h);
		g.drawString(accumLabel, labelX, labelY += h);
		g.drawString(stepCountLabel, labelX, labelY += h);
		
		labelY = 0;
		
		g.drawString(elapsedValue, valueX, labelY += h);
		
		if (!isRunningRealTime()) {
			Color c = g.getColor();
			g.setColor(Color.RED);
			g.drawString(tcmpValue + " (limited)", valueX, labelY += h);
			g.setColor(c);
		} else {
			g.drawString(tcmpValue, valueX, labelY += h);
		}
		
		g.drawString(stepValue, valueX, labelY += h);
		g.drawString(accumValue, valueX, labelY += h);
		g.drawString(stepCountValue, valueX, labelY += h);
		
		g.setTransform(tr);
	}
	
	public void increaseTimeCompression() {
		if (isRunningRealTime) {
			timeCompression *= 2;
		}
	}
	
	public void decreaseTimeCompression() {
		timeCompression /= 2;
	}
	
	public synchronized void resetTimeCompression() {
		timeCompression = 1;
	}
	
	public double getTimeCompression() {
		return timeCompression;
	}
	
	public double getTime() {
		return simTime;
	}
	
	public double getTimeStep() {
		return dt;
	}
	
	public long getStepCount() {
		return stepCount;
	}
	
	/**
	 * Gets the ratio between accumulated time and the time step size. Essentially it returns how 
	 * close the simulator is to perform a time step, typically in [0, 1].
	 * 
	 * If the time between simulation steps is fairly big, this can be used to interpolate between two states 
	 * when rendering, so as to make the animation smooth, even when the simulation is running slowly.
	 * 
	 * @return The ratio of accumulated time to time step size.
	 */
	public double getAccumulatorRatio() {
		return simTimeAccum / dt;
	}
	
	public void setDtDownLock(boolean b) {
		dtDownLock = b;
	}
	
	public boolean isDtDownLock() {
		return dtDownLock;
	}
	
	public void setDtUpLock(boolean b) {
		dtUpLock = b;
	}
	
	public boolean isDtUpLock() {
		return dtUpLock;
	}
	
	public boolean isRunningRealTime() {
		return isRunningRealTime;
	}
	
	public boolean isRunning() {
		return true;
	}
	
	public boolean isPaused() {
		return isPaused;
	}
	
	public boolean isResetting() {
		return false;
	}
	
	public boolean hasTerminated() {
		return false;
	}
	
	public void start() {
		sw.reset();
		isPaused = false;
	}
	
	public void pause() {
		isPaused = true;
	}
	
	public void reset() {
		// Re-init simulator.
		init();
	}
	
	public void terminate() {
		// Not applicable for an unthreaded simulator.
	}
	
	public void setGameController(GameController game) {
		this.game = game;
	}
}
